2012-12-02 19:56:00
前面九月份的八篇关于COM的文章,说的都是进程内COM。那时,我们从一个含内嵌IE控件的窗口说起,根据COM协议手工书写了进程内COM组件,并由此积累了一些类似ATL的框架性代码。
今天开始,我们把脚步迈向进程外组件。同样是从最基础的开始,本篇我们将根据进程外COM组件的加载规范手工编写一个EXE,然后用标准的COM调用方法来使用它。之前积累的框架性代码不属于第三方库,所以这里不会避免去使用,相反地,会把一些通用性较强的代码直接扩充到框架中。
本文仅限于常规EXE。NT服务程序暂时不在讨论之列。
进程外COM不像DLL,不需要实现四个导出函数。取而代之地,需要实现一些命令行参数:
我没有找到官方文档,只是从ATL的实现来找到了上述四个参数。从名字来看,很容易理解。前两个相当于进程内组件的 DllRegisterServer 和 DllUnregisterServer,后两个针对当前用户,相当于 DllInstall(“user”)。
从ATL的实现代码来看,命令的前导符号不必是“/”,也可以是“-”。
另外,从测试情况来看,当COM库加载进程外组件的时候,会带上参数“-Embedding”,这可以用于区分用户主动运行还是被COM库加载。
为了快速达到运行目的,今天我们只实现/RegServer和/UnregServer,后两个先不管了。
进程外COM和进程内COM的注册表结构大体一致,“粗看”发现,唯一的区别是,CLSID下的InprocServer32变成了LocalServer32。另外一个关键点是,我们自定义的每一个接口都需要注册到Interface下。Interface键结构:

第二个,TypeLib,跟CLSID的TypeLib一样。 第一个,ProxyStubClsid32,是要注册该接口的代理存根对象,用于序列化/反序列化参数和返回值。序列化/反序列化在COM中的术语是列集/散集,超不喜欢这名字。我们这里不实现自定义的代理存根,直接写死“{00020424-0000-0000-C000-000000000046}”,用系统的。不过使用这个代理存根有个局限,接口必须符合下列两种情况之一:
另外说一点,ATL的 /RegServer,仅仅注册 dual 的接口。这点我们这里不学。
下面修改以前的ComModule::RegisterTypeLib,增加注册Interface的代码:
1bool RegisterTypeLib(HKEY hRootKey)
2{
3 String strPath;
4 strPath += _T("Software\\Classes\\TypeLib\\");
5 strPath += m_strLibID;
6 strPath += _T("\\");
7 strPath += m_strLibVersion;
8
9 if (!Registry::SetString(hRootKey, strPath, _T(""), m_strLibName))
10 {
11 return false;
12 }
13
14 strPath += _T("\\0\\");
15#ifdef _WIN64
16 strPath += _T("Win64");
17#else
18 strPath += _T("Win32");
19#endif
20
21 if (!Registry::SetString(hRootKey, strPath, _T(""), m_strModulePath))
22 {
23 return false;
24 }
25
26 for (UINT i = 0; i < m_pTypeLib->GetTypeInfoCount(); ++i)
27 {
28 TYPEKIND type = TKIND_MAX;
29 HRESULT hr = m_pTypeLib->GetTypeInfoType(i, &type);
30
31 if (FAILED(hr))
32 {
33 return false;
34 }
35
36 if (type != TKIND_INTERFACE && type != TKIND_DISPATCH)
37 {
38 continue;
39 }
40
41 ITypeInfo *pTypeInfo = nullptr;
42 hr = m_pTypeLib->GetTypeInfo(i, &pTypeInfo);
43
44 if (FAILED(hr))
45 {
46 return false;
47 }
48
49 XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::Release);
50
51 TYPEATTR *pAttr = nullptr;
52 pTypeInfo->GetTypeAttr(&pAttr);
53
54 if (FAILED(hr))
55 {
56 return false;
57 }
58
59 XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::ReleaseTypeAttr, pAttr);
60
61 TCHAR szInterfaceID[40] = {};
62 StringFromGUID2(pAttr->guid, szInterfaceID, ARRAYSIZE(szInterfaceID));
63
64 String strInterfacePath;
65 strInterfacePath += _T("Software\\Classes\\Interface\\");
66 strInterfacePath += szInterfaceID;
67
68 if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\ProxyStubClsid32"), _T(""), _T("{00020424-0000-0000-C000-000000000046}")))
69 {
70 return false;
71 }
72
73 if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\TypeLib"), _T(""), m_strLibID.GetAddress()))
74 {
75 return false;
76 }
77
78 if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\TypeLib"), _T("Version"), m_strLibVersion.GetAddress()))
79 {
80 return false;
81 }
82 }
83
84 return true;
85}
反注册相应地增加删除Interface的代码:
1bool UnregisterTypeLib(HKEY hRootKey)
2{
3 for (UINT i = 0; i < m_pTypeLib->GetTypeInfoCount(); ++i)
4 {
5 TYPEKIND type = TKIND_MAX;
6 HRESULT hr = m_pTypeLib->GetTypeInfoType(i, &type);
7
8 if (FAILED(hr))
9 {
10 return false;
11 }
12
13 if (type != TKIND_INTERFACE && type != TKIND_DISPATCH)
14 {
15 continue;
16 }
17
18 ITypeInfo *pTypeInfo = nullptr;
19 hr = m_pTypeLib->GetTypeInfo(i, &pTypeInfo);
20
21 if (FAILED(hr))
22 {
23 return false;
24 }
25
26 XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::Release);
27
28 TYPEATTR *pAttr = nullptr;
29 pTypeInfo->GetTypeAttr(&pAttr);
30
31 if (FAILED(hr))
32 {
33 return false;
34 }
35
36 XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::ReleaseTypeAttr, pAttr);
37
38 TCHAR szInterfaceID[40] = {};
39 StringFromGUID2(pAttr->guid, szInterfaceID, ARRAYSIZE(szInterfaceID));
40
41 String strInterfacePath;
42 strInterfacePath += _T("Software\\Classes\\Interface\\");
43 strInterfacePath += szInterfaceID;
44
45 if (!Registry::DeleteKeyRecursion(hRootKey, strInterfacePath))
46 {
47 return false;
48 }
49 }
50
51 String strPath;
52 strPath += _T("Software\\Classes\\TypeLib\\");
53 strPath += m_strLibID;
54
55 if (!Registry::DeleteKeyRecursion(hRootKey, strPath))
56 {
57 return false;
58 }
59
60 return true;
61}
再给RegisterComClasses新增个参数:
1bool RegisterComClasses(HKEY hRootKey, bool bInprocServer = true)
相应注册逻辑修改如下:
1if (bInprocServer)
2{
3 if (!Registry::SetString(hRootKey, strClassIDPath + _T("\\InprocServer32"), _T(""), m_strModulePath))
4 {
5 return false;
6 }
7}
8else
9{
10 if (!Registry::SetString(hRootKey, strClassIDPath + _T("\\LocalServer32"), _T(""), m_strModulePath))
11 {
12 return false;
13 }
14}
然后,汇总一下,写两个供调用的接口函数:
1STDMETHODIMP ExeRegisterServer()
2{
3 if (!RegisterTypeLib(HKEY_LOCAL_MACHINE))
4 {
5 return E_FAIL;
6 }
7
8 if (!RegisterComClasses(HKEY_LOCAL_MACHINE, false))
9 {
10 return E_FAIL;
11 }
12
13 return S_OK;
14}
15
16STDMETHODIMP ExeUnregisterServer()
17{
18 if (!UnregisterComClasses(HKEY_LOCAL_MACHINE))
19 {
20 return E_FAIL;
21 }
22
23 if (!UnregisterTypeLib(HKEY_LOCAL_MACHINE))
24 {
25 return E_FAIL;
26 }
27
28 return S_OK;
29}
对于PerUser的情形,只需要把四处HKEY_LOCAL_MACHINE换成HKEY_CURRENT_USER就好了,不过现在我们先不管这些。
然后我们转到入口函数WinMain,加上对/RegServer和/UnregServer的处理:
1int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
2 _In_opt_ HINSTANCE hPrevInstance,
3 _In_ LPTSTR lpCmdLine,
4 _In_ int nCmdShow)
5{
6 xl::g_pComModule = new xl::ComModule(hInstance, _T("Streamlet COMProvider TypeLib 1.0"));
7
8 if (_tcsicmp(lpCmdLine, _T("/RegServer")) == 0 || _tcsicmp(lpCmdLine, _T("-RegServer")) == 0)
9 {
10 xl::g_pComModule->ExeRegisterServer();
11 }
12 else if (_tcsicmp(lpCmdLine, _T("/UnregServer")) == 0 || _tcsicmp(lpCmdLine, _T("-UnregServer")) == 0)
13 {
14 xl::g_pComModule->ExeUnregisterServer();
15 }
16
17 delete xl::g_pComModule;
18
19 return 0;
20}
到目前为止,可以编译程序,把组件注册上了。
进程外COM的启动大致有如下几步:
我们将WinMain改成如下的样子:
1int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
2 _In_opt_ HINSTANCE hPrevInstance,
3 _In_ LPTSTR lpCmdLine,
4 _In_ int nCmdShow)
5{
6 xl::g_pComModule = new xl::ComModule(hInstance, _T("Streamlet COMProvider TypeLib 1.0"));
7
8 if (_tcsicmp(lpCmdLine, _T("/RegServer")) == 0 || _tcsicmp(lpCmdLine, _T("-RegServer")) == 0)
9 {
10 xl::g_pComModule->ExeRegisterServer();
11 }
12 else if (_tcsicmp(lpCmdLine, _T("/UnregServer")) == 0 || _tcsicmp(lpCmdLine, _T("-UnregServer")) == 0)
13 {
14 xl::g_pComModule->ExeUnregisterServer();
15 }
16 else if (_tcsicmp(lpCmdLine, _T("/Embedding")) == 0 || _tcsicmp(lpCmdLine, _T("-Embedding")) == 0)
17 {
18 HRESULT hr = xl::g_pComModule->ExeRegisterClassObject();
19
20 if (SUCCEEDED(hr))
21 {
22 MSG msg = {};
23
24 while (GetMessage(&msg, nullptr, 0, 0))
25 {
26 TranslateMessage(&msg);
27 DispatchMessage(&msg);
28 }
29 }
30
31 xl::g_pComModule->ExeUnregisterClassObject();
32 }
33
34 delete xl::g_pComModule;
35
36 return 0;
37}
我们将在ComModule::ExeRegisterClassObject里完成1、2,在ComModule::ExeUnregisterClassObject里完成4、5。
之前为了注册COM类,我们已经保存了对象表,里面有每一个对外暴露的类的CLSID、类厂创建函数指针等。我们找到每一个要注册的类,创建类厂,然后调用CoRegisterClassObject。CoRegisterClassObject将返回一个DWORD值(Cookie),用于唯一确定所注册的类,在CoRevokeClassObject的时候要用到,所以要保存起来。CoRegisterClassObject内部会将我们传给它的类厂的引用计数加一,我们应该把自己产生的引用计数都释放掉,整个COM组件运行期间,类厂引用计数始终维持在1,就是COM库占用的那个。
代码如下:
1STDMETHODIMP ExeRegisterClassObject()
2{
3 HRESULT hr = CoInitialize(nullptr);
4
5 if (FAILED(hr))
6 {
7 return hr;
8 }
9
10 for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
11 {
12 if (*ppEntry == nullptr)
13 {
14 continue;
15 }
16
17 IClassFactory *pClassFactory = (*ppEntry)->pfnCreator();
18
19 if (pClassFactory == nullptr)
20 {
21 return E_FAIL;
22 }
23
24 IUnknown *pUnk = nullptr;
25 HRESULT hr = pClassFactory->QueryInterface(__uuidof(IUnknown), (LPVOID *)&pUnk);
26
27 if (FAILED(hr) || pUnk == nullptr)
28 {
29 return hr;
30 }
31
32 DWORD dwRegister = 0;
33 hr = CoRegisterClassObject(*(*ppEntry)->pClsid,
34 pUnk,
35 CLSCTX_LOCAL_SERVER,
36 REGCLS_MULTIPLEUSE,
37 &dwRegister);
38 pUnk->Release();
39
40 if (FAILED(hr))
41 {
42 return hr;
43 }
44
45 m_arrRegClassObjects.PushBack(dwRegister);
46 }
47
48 return S_OK;
49}
这个就比较简单了,针对上面保存的Cookie,调用CoRevokeClassObject即可。CoRevokeClassObject会调用类厂的Release来释放COM库占用的那个引用计数。
1STDMETHODIMP ExeUnregisterClassObject()
2{
3 for (auto it = m_arrRegClassObjects.Begin(); it != m_arrRegClassObjects.End(); ++it)
4 {
5 CoRevokeClassObject(*it);
6 }
7
8 m_arrRegClassObjects.Clear();
9
10 CoUninitialize();
11
12 return S_OK;
13}
通过上面几个步骤,我们的进程外COM组件已经可以被使用了。为了检验参数的序列化/反序列化是否正确,我们稍稍地改变下接口ISampleInterface,加些参数:
[
object,
uuid(83C783E3-F989-4E0D-BFC5-631273EDFFDB),
dual,
]
interface ISampleInterface : IDispatch
{
[id(1)] HRESULT SampleMethod([in] BSTR bstrMessage, [out] LONG *pResult);
};
并实现如下:
1STDMETHODIMP SampleClass::SampleMethod(BSTR bstrMessage, LONG *pResult)
2{
3 MessageBox(NULL, bstrMessage, _T("Info"), MB_OK | MB_ICONINFORMATION);
4
5 if (pResult != nullptr)
6 {
7 *pResult = 12345678;
8 }
9
10 return S_OK;
11}
另起一个EXE:
1int _tmain(int argc, TCHAR *argv[])
2{
3 HRESULT hr = CoInitialize(NULL);
4
5 if (FAILED(hr))
6 {
7 return 0;
8 }
9
10 ISampleInterface *pSampleInterface = nullptr;
11 hr = CoCreateInstance(__uuidof(SampleClass),
12 nullptr,
13 CLSCTX_LOCAL_SERVER,
14 __uuidof(ISampleInterface),
15 (LPVOID *)&pSampleInterface);
16
17 if (SUCCEEDED(hr))
18 {
19 BSTR bstrMessage = SysAllocString(_T("COMProvider!SampleClass::SampleMethod Called From COMUser."));
20 LONG nResult = 0;
21 pSampleInterface->SampleMethod(bstrMessage, &nResult);
22 SysFreeString(bstrMessage);
23 pSampleInterface->Release();
24 }
25
26 CoUninitialize();
27
28 return 0;
29}
运行结果:

还有一个输出参数值:

一切正常。
但是此时,调用方运行结束后,COM组件还在运行中,没有退出。退出是需要我们手工控制的,下面我们来做这件事。
观察了下ATL的实现,它是在最后一个对象被释放后,触发AtlExeModuleT的Unlock,在其中向主线程发送了一个WM_QUIT,结束消息循环。
我们之前实现进程内组件的时候,也做过DllCanUnloadNow,这里面,有一个对象计数和类厂的锁计数。而由于xl::ComClass在构造的时候就对对象技术进行了加一,所以对象计数包含了类厂的计数,这在进程内组件里没问题,而且也是必须的。因为类厂存在,说明被使用,DLL不该被释放。
而在进程外组件中,由于消息循环结束之前,COM库肯定会占用一个类厂引用计数,如果对象计数包含类厂的话,我们就无法判断发WM_QUIT的时机了。因此,我们这里对xl::ComClass的构造函数和加一个参数,指定需不需要加引用计数,然后分别在DLL创建类厂和EXE创建类厂的时候传入不同的值。
xl::ComClass 构造析构函数修改如下:
1ComClass(bool bAddObjRefCount = true) : m_nRefCount(0), m_bAddObjRefCount(bAddObjRefCount)
2{
3 if (g_pComModule != nullptr && m_bAddObjRefCount)
4 {
5 g_pComModule->ObjectAddRef();
6 }
7}
8
9~ComClass()
10{
11 if (g_pComModule != nullptr && m_bAddObjRefCount)
12 {
13 g_pComModule->ObjectRelease();
14 }
15}
类厂构造函数和创建函数修改如下:
1static IClassFactory *CreateFactory(bool bAddObjRefCount = true)
2{
3 return new ClassFactory(bAddObjRefCount);
4}
5
6ClassFactory(bool bAddObjRefCount = true) :
7 ComClass<ClassFactory<T>>(bAddObjRefCount)
8{
9
10}
11
12xl::ComModule::ExeRegisterClassObject中修改如下:
13
14STDMETHODIMP ExeRegisterClassObject()
15{
16 HRESULT hr = CoInitialize(nullptr);
17
18 if (FAILED(hr))
19 {
20 return hr;
21 }
22
23 for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
24 {
25 if (*ppEntry == nullptr)
26 {
27 continue;
28 }
29
30 IClassFactory *pClassFactory = (*ppEntry)->pfnCreator(false);
31
32 if (pClassFactory == nullptr)
33 {
34 return E_FAIL;
35 }
36
37 IUnknown *pUnk = nullptr;
38 HRESULT hr = pClassFactory->QueryInterface(__uuidof(IUnknown), (LPVOID *)&pUnk);
39
40 if (FAILED(hr) || pUnk == nullptr)
41 {
42 return hr;
43 }
44
45 DWORD dwRegister = 0;
46 hr = CoRegisterClassObject(*(*ppEntry)->pClsid,
47 pUnk,
48 CLSCTX_LOCAL_SERVER,
49 REGCLS_MULTIPLEUSE,
50 &dwRegister);
51 pUnk->Release();
52
53 if (FAILED(hr))
54 {
55 return hr;
56 }
57
58 m_arrRegClassObjects.PushBack(dwRegister);
59 }
60
61 m_dwThreadId = GetCurrentThreadId();
62
63 return S_OK;
64}
注意在最后存了一个Thread ID,这个ID表明当前是处于EXE模式。
然后在xl::ComModule的对象引用计数释放、锁计数释放的函数中判断并发送WM_QUIT:
1ULONG STDMETHODCALLTYPE ObjectRelease()
2{
3 ULONG lResult = (ULONG)InterlockedDecrement(&m_nObjectRefCount);
4
5 if (m_dwThreadId != 0 && CanUnloadNow())
6 {
7 PostThreadMessage(m_dwThreadId, WM_QUIT, 0, 0);
8 }
9
10 return lResult;
11}
12
13ULONG STDMETHODCALLTYPE LockRelease()
14{
15 ULONG lResult = (ULONG)InterlockedDecrement(&m_nLockRefCount);
16
17 if (m_dwThreadId != 0 && CanUnloadNow())
18 {
19 PostThreadMessage(m_dwThreadId, WM_QUIT, 0, 0);
20 }
21
22 return lResult;
23}
其中CanUnloadNow跟DllCanUnloadNow的判断是一致的,于是乎合并起来:
1bool CanUnloadNow()
2{
3 if (m_nObjectRefCount > 0 || m_nLockRefCount > 0)
4 {
5 return false;
6 }
7
8 return true;
9}
10
11STDMETHODIMP DllCanUnloadNow()
12{
13 return CanUnloadNow() ? S_OK : S_FALSE;
14}
好了,代码实现全部结束。
和以前差不多,也写个VBS脚本:
Set obj = WScript.CreateObject("Streamlet.COMProvider.SampleClass.1")
obj.SampleMethod "Hello! Calling from VBScript.", 0
运行结果:

VB6代码:
Private Sub Command1_Click()
Dim obj As Object
Set obj = CreateObject("Streamlet.COMProvider.SampleClass.1")
obj.SampleMethod "Hello! Calling from VB6.", 0
Set obj = Nothing
End Sub
运行结果:

JS代码如下:
1<script type='text/javascript'>
2 var objCom = new ActiveXObject("Streamlet.COMProvider.SampleClass.1");
3 objCom.SampleMethod("Hello! Calling from JavaScript.", 0);
4</script>
运行结果:

遗憾的是,从网页调用后,COM组件似乎没法退出。查了下,貌似是JS释放对象机制的问题。
本文例子代码见:COMProtocol5.rar(http://pan.baidu.com/s/1dD3ZzUD)
首发:http://www.cppblog.com/Streamlet/archive/2012/12/02/195900.html